[Kodowanie Windows-1250, koniec wiersza CR+LF, test: zażółć gęślą jaźń] ================================================================================ B A Z Y D A N Y C H W D E L P H I ================================================================================ Adam Sawicki regedit.gamedev.pl 16 czerwca 2006 Czyli jak się tworzy w Delphi bazy danych. To jest pomoc dydaktyczna do pewnego egzaminu. Będzie bardzo zwięzła. Trzeba już umieć obsługiwać Delphi i znać Object Pascala. To będzie tylko o bazach danych. Instrukcja obsługi: 1. przeczytać 2. zrozumieć 3. samemu sobie poćwiczyć w domu 4. iść na egzamin 5. zdać. Autor sam się na tym nie zna, nauczył się na ile potrafił i nie ponosi żadnej odpowiedzialności za bzdury które tu powypisywał. Czytasz na własną odpowiedzialność :) 1. Wstęp ================================================================================ Database Desktop ---------------- Zaczynamy tak: W Delphi menu Tools > Database Desktop. Odpala się taki śmieszny, brzydki program. W nim można tworzyć tabelki. Najpierw trzeba File > Working Directory i tam wpisać katalog w którym będzie twój program. Potem File > Private Directory i to samo. Teraz można już File > New > Table. Typ bazy danych? Nie wiem, zostawiam domyślny o wielce wymownej nazwie "Paradox" :) Tu można ułożyć strukturę tabeli, czyli wprowadzić jakie będą pola. Pole ma nazwę i typ i ewentualnie rozmiar (stosuje się do stringów, to będzie max. długość w znakach) i czy jest kluczem. Na jakie typy warto zwrócić uwagę: - Autoincrement - liczba całkowita, która sama się będzie przypisywała i nie trzeba tego robić ręcznie - Long integer - to jest ten zwykły int - Number - to jest liczba rzeczywista - Alpha - to jest string - Binary - to jest BLOB Układamy, potem Save as i zapisujemy gdzieś pod jakąś nazwą w tym katalogu. UWAGA! Przed powrotem do Delphi zamykamy ten program, bo inaczej "katalog będzie zablokowany" i guzik zdziałamy. Delphi ------ Paleta komponentów > zakładka "BDE" > komponent Table. To jest tabela. W niej ustawiamy dwie rzeczy: 1. DatabaseName - tu może być: - albo alias (w Database Desktop można robić aliasy do baz danych i wtedy one są chyba wspólne dla całego systemu), - albo ścieżka do twojego programu, np. "C:\Temp\MyExam". Tylko uwaga! Tu nie działa ścieżka względna, żadne ".\" itp., musi być bezwzględna. Z kodu można to przypisać tak: Table1.DatabaseName = ExtractFilePath(Application.ExeName); 2. TableName - tu ma być nazwa tabeli. Żeby odpalić tabelę, trzeba ustawić jej Active na True. Można to zrobić już na etapie projektowania ale radzili żeby raczej to robić w czasie wykonania, np. w OnCreate: Table1.Active := True; Następna sprawa. Paleta komponentów > zakładka "Data Access" > komponent DataSource. To jest taka zwrotnica przez którą przechodzą dane dalej. Ustawiamy mu DataSet na Table1. Ostatnia rzecz to komponenty. Tu opcje są dwie. Możemy postawić osobne kontrolki do różnych pól i wtedy będzie w nich widać wartości z bieżąceego rekordu. Są do tego komponenty na zakładce "Data Controls". Jest tam EDIT, MEMO, LABEL, IMAGE, różne rzeczy, podobne do tych standardowych tylko bazodanowe. Przypisujemy w nich: 1. DataSource na DataSource1, 2. DataField na nazwę konkretnego pola tej tabeli. Opcja druga to bardzo fajny komponent DBGrid - pierwszy na zakładce "Data Controls". To jest tabelka w której widać wszystko z tabeli i można też tam edytować. Do nawigacji po tabeli stawiamy DBNavigator - komponent drugi z zakładki "Data Controls". Przypisujemy mu DataSource na DataSource1 i gotowe. Można jeszcze ShowHint na True. Są tam przyciski do przechodzenia po rekordach, wstawiania, usuwania, edytowania, zatwierdzania i anulowania zmian. Odpalamy program i gotowe! Baza danych w Delphi wyklikana. Można w ten sposób zrobić sobie i postawić w programie kilka niezależnych tabel. Teraz jedziemy dalej... 2. TField ================================================================================ TField to klasa bazowa reprezentująca pola. Nie występuje sama. Tabela ma listę pól. Dzięki tej klasie można odczytywać i zapisywać wartości poszczególnych pól w bieżącym rekordzie danej tabeli. Jak używać wartości pola? Załóżmy dla dowodu :) że mamy już obiekt klasy TField. Teraz są takie opcje: - Value: Variant Variant to taki typ dynamiczny (coś jak zmienne w PHP) że możesz sobie go odczytywać i przypisywać jakom string, liczbę, co chcesz i się nie przejmować. - AsVariant Działa jak wyżej. - AsString, AsInteger itp. Używasz wartości pola jakby była danego typu. Jeśli nie jest, to się spróbuje skonwertować. - Table1.FieldValues['Pole1']: Variant Szybki dostęp do poszczególnych pól tabeli w postaci Viariantów. Skąd wziąć obiekt klasy TField? - Jeśli pole jest typu Presistant Field, to jest normalnie polem twojej klasy formatki TForm1 tak jak komponenty i możesz po prostu użyć jego nazwy, np. Table1ID.cośtam. - Table1.Fields[0] Poszczególne pola tabeli używając numeru. - Table1.FieldByName('Pole1') Po nazwie. - Table1.IndexFields[0] Któreśtam z kolei pole będące indeksem. Pola mogą być Dynamic albo Presistant. Normalnie tabela ma pola Dynamic, czyli sobie sama tworzy w momencie aktywacji te obiekty klasy TField dokładnie takie, jakie są pola w tabeli. A teraz uwaga bo będzie Mega Wypas [tm]! Klikamy dwa razy lewym na komponencie tabelki. Pokazuje się taki śmieszny edytorek pól. Na tej liście prawym i mamy menu kontekstowe, a w nim możemy dodawać pola. Add fields po prostu doda nam wszystkie pola tak jak są w tabeli. Fajnie. Od tej pory, skoro ta lista nie jest pusta, nie ma już pól dynamicznych. There is no Dynamic Field :) Są tylko te które my tutaj zdefiniujemy i to są Presistant Fields. Jak widać każde ma masę właściwości, jak choćby alignację tekstu. Można też dodawać nowe pola. Jest kilka rodzajów pól i właśnie tutaj jak sądzę leży rozwiązanie tych różnych zadań które mogą nam zadać. 1. Data To zwykłe pole biorące dane z jakiegoś pola tabeli i już :P 2. Calculated To pole, którego wartość będzie wyliczana. Jak to wyliczać? Robisz obsługę zdarzenia OnCalcFields dla tabeli i tam centralnie dajesz obliczanie jakiegoś pola, o tak: Table1.FieldValues['IDx2'] := Table1.FieldValues['ID'] * 2; 3. Aggregate Tego akurat nie umiem. Może się nie przyda :) 4. Lookup Takie pole może brać dane z innej tabeli. Jakby to wytłumaczyć... Załóżmy, że mamy takie dwie tabele: Studenci ID Imie Nazwisko Oceny ID_studenta Ocena Teraz tak: Dajemy w tabeli z ocenami Presistant Field "Imie". Przy jego tworzeniu podajemy takie dane: - Key Fields = ID_studenta czyli pole mojej tabeli, po którym będę łączyć - DataSet = DataSet2 czyli tam gdzie są pobierane dane z tej drugiej tabeli - ze studentami - Lookup Keys = ID czyli pole tej drugiej tabeli, według którego je łączę - Result Field = Imie czyli pole tej drugiej tabeli, z którego wybieram dane To samo można zrobić dla drugiego Presistant Field - "Nazwisko" i już tabela z ocenami oprócz oceny i nic nie mówiącego identyfikatora studenta ma też jego imię i nazwisko i co więcej - nadal jest edytowalna, a nawet te pola Imie i Nazwisko da się edytować i pojawiają się tam listy rozwijalne (ComboBoxy). Ale power! :D 3. TDataSet ================================================================================ TDataSet - tak się nazywa abstrakcyja klasa bazowa dla różnych rzeczy, które zapodają dane w postaci tabeli. To może być prawdziwa tabela bazy danych, wynik wykonania zapytania select albo procedura składowana. Koniec wstępu teoretycznego :) Jak dla nas, z tej klasy dziedziczą (a tym samym mają wszystko to co ona) TTable oraz TQuery. Co ta klasa potrafi? Po pierwsze - trzeba ją uaktywnić. Robi to albo metoda Open i Close, albo ustawienie właściwości Active odpowiednio na True czy False. Po drugie, w każdej chwili DataSet jest w jakimś *stanie* (właściwość State). Główne stany to: - dsInactive - nie uaktywniona - dsBrowse - przeglądanie danych - dsEdit - edytowanie bieżącego rekordu - dsInsert - edytowanie nowego, nieistniejącego jeszcze rekordu celem dodania - dsSetKey - ustawianie danych po których będziemy szukali i jeszcze masa innych... Na szczęście zwykle nie trzeba się tym zajmować. Po trzecie, w DataSet zawsze jakiś jeden rekord jest bieżący i to na nim możemy operować. - RecNo - przechowuje numer bieżacego rekordu - First, Last, Prior, Next - wiadomo co robią :) - MoveBy - przesuwa kursor o ileśtam rekordów w dół albo w górę (jeśli minus) - Bof, Eof - zwraca true jeśli odpowiednio jesteśmy na początku albo na końcu Teraz jak edytować dane: - CanModify - mówi czy DataSet da się modyfikować - Edit - wprowadza w stan edycji Od tej pory można zmieniać pola. - Append, Insert - wprowadza w stan edycji nowego rekordu Różnica jest taka, że Append dodaje nowy rekord na końcu, a Insert nie. - Post - zatwierdza wprowadzone zmiany - Cancel - anuluje wprowadzone zmiany - Delete - kasuje bieżący rekord Przykład jak zmienić bieżący rekord: Table1.Edit; Table1.FieldByName('ID').AsInteger := 666; Table1.FieldByName('Imie').AsString := 'Jan'; Table1.FieldByName('Nazwisko').AsString := 'Kowalski'; Table1.Post; Inny sposób to metody: AppendRecord, InsertRecord, SetFields. Dwie pierwsze dodają nowy rekord, ostatnia zmiena bieżący. Podaje się do nich wszystkie pola na raz, o tak: Table1.Edit; Table1.SetFields([666, 'Jan', 'Kowalski']); Table1.Post; Jeśli chcesz robić coś na kolejnych rekordach i nie chcesz żeby kontrolki migały pokazując stale co się dzieje i opóźniając wszystko, użyj na tabeli metod DisableControls i EnableControls. Szukanie -------- Sposób 1: Metoda Locate. Skacze do pierwszego/następnego rekordu, którego podane pole ma podaną wartość. Na przykład: Table1.Locate('Imie', 'Jan', [loPartialKey]); Opcje w ostatnim parametrze mogą być takie: - loCaseInsensitive - wiadomo - loPartialKey - to znaczy chyba żeby można było podać tylko kawałek stringa Szukanie po kilku polach na raz: Table1.Locate('Imie;Nazwisko', VarArrayOf(['Jan', 'Kowalski']), [loPartialKey]); Jest też jakaś metoda Lookup, ale nie zgłębiałem tego tematu. Sposób 2: Po indeksach. Używamy tutaj tylko pól które są indeksami w tabeli. Table1.SetKey; Table1.FieldByName('ID').AsInteger := 2; Table1.GotoKey; Jak to działa? SetKey wprowadza tabelę w specjalny stan. Dzięki niemu w następnej linii zamiast modyfikować wartość pola bieżącego rekordu, podajemy wartość klucza po którym chcemy szukać. GotoKey kończy szukanie skacząc do znalezionego rekordu. Jest też GotoNearest. EditKey działa podobnie jak SetKey ale pozwala zmienić, a nie ustawiać od nowa wartości kluczy do wyszukiwania. Z kolei metody FindKey i FindNearest pozwalając na bardziej skróconą formę wyszukiwania - o tak: Table1.FindKey([2]); Constraints ----------- Można nałożyć ograniczenia na wartości pól kiedy użytkownik będzie je edytował. Prosta sprawa. Tabela ma właściwość Constraints. Wchodzimy tam i jest edytor. Dodajemy nowe ograniczenie i przypisujemy w nim: 1. CustomConstraint Jakieś wyrażenie SQL-owe, np.: (Ocena >= 2) AND (Ocena <= 5) 2. ErrorMessage Komunikat błędu, jaki ma się pokazywać po jego złamaniu. Odpalamy i od razu działa. Filtry ------ Pozwalają pokazywać tylko niektóre rekordy z tabeli. Opcje są dwie: 1. Filtered = True i obsłużyć zdarzenie OnFilterRecord, a w nim, jak widać, dostajemy DataSet żeby odczytać z niego wartości pól bieżącego rekordu, możemy wykonać dowolny kod Delphi i wreszcie przypisać odpowiednią wartość do parametru wyjściowego Accept. 2. Filtered = True i wypełnić właściwość Filter. Tam można wpisać wyrażenie z operatorami takimi, jak + - * / % AND OR NOT = <> < > <= >=. Jeśli w FilterOptions nie ustawisz foNoParialCompare na True, to możesz w stringach używać gwiazdki * żeby wyszukiwać według maski. Do przeskakiwania między filtrowanymi rekordami służą metody FindFirst, FindLast, FindPrior, FindNext i Found. Bookmarks --------- Dzięki nim można sobie odhaczać jakieś rekordy celem późnejszego wrócenia do nich. Działa to tak: Deklarujemy: var Bookmark: TBookmark; Tworzymy na podstawie bieżącego rekordu: Bookmark := Table1.GetBookmark; Używamy skacząc do zapamiętanego rekordu: Table1.GotoBookmark(Bookmark); Na koniec zwalniamy: Table1.FreeBookmark(Bookmark); 4. TQuery ================================================================================ Query leży obok Table na zakładce "BDE". Służy do wykonywania zapytań SQL-owych "select" i oglądania wyników tak jakby to była tabela. Niestety, nici z jej edytowania. Nawet właściwość RequestLive, która powinna na to pozwalać, nie działa jeśli zrobisz łączenie dwóch tabel albo cokolwiek ambitniejszego niż wybieranie jakiś pól z jednej tabeli, które można zrobić zwykłym TTable :P Pierwsza sprawa to właściwość SQL. Tam siedzi zapytanie. Jest typu TStrings ze wszystkimi tego konsekwencjami. Ten typ służy do pamiętania listy łańcuchów. Dla nas interesujące mogą być: jego pole Text, które przechowuje całość jako jeden łańcuch, oraz np. metody LoadFromFile i SaveToFile. Można to zapytanie wpisać sztywno na etapie projektowania albo z kodu w taki sposób: Query1.SQL.Text := 'select ID, Imie, Nazwisko from Studenci;'; Zapytanie wykonuje się w chwili "uaktywnienia" DataSetu, czyli tak jak dla tabeli - metoda Open albo właściwość Active na True. Jest też metoda ExecSQL, jakby ktoś chciał za pomocą tego komponentu wykonać jakieś zapytanie typu insert, update czy delete, które nie zwraca tabeli z danymi. Ciekawą sprawą są parametry. Dzięki nim można wstawiać różne dane do zapytania bez modyfikowania samego zapytania. Jak to leci? 1. Zapytanie SQL wpisujemy na szytywno, a w nim np.: select ID, Imie, Nazwisko from Studenci where Imie = :p0 Uwaga! Choć to dziwne, :p0 nie jest w cudzysłowach mimo że trafi tam string. 2. Query na właściwość Params i tam muszą być zdefiniowane wszystkie parametry, ale wygląda na to, że same tam trafiają po wpisaniu zapytania na etapie projektowania. W czasie wykonania do poszczególnych parametrów odwołujemy się przez indeks za pomocą właściwości Params lub przez nazwę za pomocą ParamByName. 3. Używamy tak: Query1.Close; Query1.Params[0].AsString := Edit1.Text; Query1.Open; 5. Dynamiczne tabele ================================================================================ Tabele można (a jak każą to nawet trzeba :) tworzyć dynamicznie. Robi się to podobnie jak z dynamicznym tworzeniem zwykłych komponentów, czyli tworzy się obiekt, przypisuje się mu właściwości i już można go używać. Oto przykład: var t: TTable; // 1 begin t := TTable.Create(Self); // 2 t.DatabaseName := ExtractFilePath(Application.ExeName); // 3 t.TableName := 'Tabela1'; t.TableType := ttDBase; t.FieldDefs.Add('ID', ftInteger, 0, true); // 4 t.FieldDefs.Add('Imie', ftString, 32, true); t.IndexDefs.Add('', 'ID', [ixPrimary]); // 5 if (not t.Exists) then // 6 t.CreateTable; // 7 DataSource1.DataSet := t; // 8 t.Active := true; // 9 Co my tu mamy: 1. Definicja zmiennej z tabelą 2. Dynamiczne utworzenie obiektu tabeli 3. Przypisanie różnych rzeczy 4. Pododawanie pól. Niektóre dostępne typy pól to: ftString, ftInteger, ftBoolean, ftFloat, ftDate, ftTime, ftDateTime, ftBlob, ftMemo 5. Dodanie indeksu 6. Tak można sprawdzić, czy plik odpowiadający definiowanej dynamicznie tabeli już na dysku nie istnieje 7. Jeśli nie, to tak się go tworzy 8. Powiązanie tabeli z DataSource, który zapewne jest postawiony na formatce 9. Odpalenie tabeli 6. BLOB ================================================================================ Pole typu BLOB (dla zainteresowanych: to skrót od Binary Large Object) może przechowywać dowolne i dowolnie długie dane binarne. Dostęp do niego mamy na takie sposoby: 1. Sposób który nam podali, a więc klasa TBlobStream. To pochodna z klasy TStream, czyli klasy bazowej wszystkich strumieni. Strumienie mają dużo fajnych rzeczy, m.in.: - Size - aktualna długość danych w bajtach - Position - aktualna pozycja kursora - Read, Write - zapis i odczyt danych binarnych - SetSize - zmiana rozmiaru - Seek - zmiana pozycji - Truncate - obcięcie do bieżącej pozycji tak że w tym miejscu będzie koniec - CopyFrom - kopiuje dane z innego strumienia Są też inne klasy strumieni np. TMemoryStream czy TFileStream. Sposób użycia jest następujący: 1. Dogrzebujemy się do obiektu klasy TField 2. Rzutujemy go na klasę TBlobField 3. Z niej tworzymy TBlobStream 4. Używamy sobie strumienia 5. Na koniec go zwalniamy Przykłady: Sposób 1 - ze strumienia do strumienia - na przykładzie wczytywania: var b: TBlobStream; f: TFileStream; begin if OpenDialog1.Execute then begin Table1.Insert; b := TBlobStream.Create(Table1.FieldByName('Data') as TBlobField, bmWrite); f := TFileStream.Create(OpenDialog1.FileName, fmOpenRead); b.CopyFrom(f, f.Size); f.Free; b.Free; Table1.Post; end; Sposób 2 - przez bufor - na przykładzie zapisywania: var b: TBlobStream; f: TFileStream; buffer: PChar; begin if SaveDialog1.Execute then begin b := TBlobStream.Create(Table1.FieldByName('Data') as TBlobField, bmRead); f := TFileStream.Create(SaveDialog1.FileName, fmCreate); buffer := AllocMem(b.Size); b.Read(buffer^, b.Size); f.Write(buffer^, b.Size); FreeMem(buffer, b.Size); f.Free; b.Free; end; 2. Najprostszy sposób, który sam obczaiłem - w ogóle bez użycia strumienia: if OpenDialog1.Execute then begin Table1.Insert; (Table1.FieldByName('Data') as TBlobField).LoadFromFile( OpenDialog1.FileName); Table1.Post; end; if SaveDialog1.Execute then begin (Table1.FieldByName('Data') as TBlobField).SaveToFile( SaveDialog1.FileName); end; Jeśli do BLOB-a ładujesz obrazki to możesz je wyświetlać np. w komponencie BDImage. Skąd brać przykładowe obrazki? Znajdziesz je w C:\Program Files\Common Files\Borland Shared\Images\Buttons. 7. Wnioski ;) ================================================================================ Na koniec jedna uwaga. Ludziska, czytajta helpa. To naprawdę nie boli. Ja się tego wszystkiego tylko z helpa nauczyłem. Na pierwszej turze nie można mieć swoich notatek. Tak więc trochę trzeba umieć, ale na dobrą sprawę to wystarczy zajrzeć do indeksu pomocy pod hasło np. TTable, stamtąd do dowolnego pola i metody i wszystko jest ładnie napisane, nawet czasami są przykłady wykorzystania. Pozdro i powodzenia na egzaminie!